# method to correct round
class Float
  def round_to(x)
    (self * 10**x).round.to_f / 10**x
  end
end

#TODO: add errors handling and logging
class Points
  def initialize(name="tmp")
    @name = name
    @points = []
    self.min_max_update
  end

  attr_accessor :name, :max_x, :min_x, :max_y, :min_y, :points

  def min_max_update # updates all min and max values
    @max_x = nil
    @min_x = nil
    @max_y = nil
    @min_y = nil
    for elem in @points
      @max_x = elem[0] if (@max_x.nil? || @max_x < elem[0])
      @min_x = elem[0] if (@min_x.nil? || @min_x > elem[0])
      @max_y = elem[1] if (@max_y.nil? || @max_y < elem[1])
      @min_y = elem[1] if (@min_y.nil? || @min_y > elem[1])
    end
  end

  def include?(x, y) # overrided include for Points
    @points.include?([x, y]) ? true : false
  end

  def add(x, y) # adds an element if it does not exist or updates "y" value when element with such "x" value already exist
    if self.include?(x, y)
      puts "such element already exist in #{@name}: [#{x}, #{y}]"
    else
      for elem in @points
        self.delete(elem[0], elem[1]) if elem[0] == x
      end
      @points.push([x, y])
      #self.min_max_update # this method is esear but works slower
      @max_x = x if (@max_x.nil? || @max_x < x)
      @min_x = x if (@min_x.nil? || @min_x > x)
      @max_y = y if (@max_y.nil? || @max_y < y)
      @min_y = y if (@min_y.nil? || @min_y > y)
      self.sort
    end
  end

  def delete(x, y) # deletes an element
    if self.include?(x, y)
      @points.delete([x, y])
      self.min_max_update
    else
      puts "no such element in #{@name}: [#{x}, #{y}]"
    end
  end

  def clear # clears all Points data
    @points = []
    self.min_max_update
  end

  def sort # just sorts
    @points = @points.sort_by{|elem| elem[0]}
  end

  def upsample # upsample Points, returns only odd points
    @points.push(@points[-1]) if @points.size%2 == 0
    upsampled = Points.new(@name + "_upsampled")
    (@points.size/2 + 1).times{ |i|
      upsampled.add(@points[i*2][0], @points[i*2][1])
    }
    return upsampled
  end

  def downsampe_default # x_k=(x_(k-1) + x_(k+1))/2, y_k=y_(k-1)
    upsampled = self.upsample
    downsampled = Points.new(@name + "_downsampledold")
    @points.size.times{ |i|
      if i%2 == 0
        downsampled.add(upsampled.points[i/2][0], upsampled.points[i/2][1])
      else
        downsampled.add((upsampled.points[i/2][0] + upsampled.points[i/2 + 1][0])*1.0/2, upsampled.points[i/2][1])
      end
    }
    return downsampled
  end

  def downsample_new # x_k=(x_(k-1) + x_(k+1))/2, y_k=(y_(k-1) + y_(k+1))/2
    upsampled = self.upsample
    downsampled = Points.new(@name + "_downsamplednew")
    @points.size.times{ |i|
      if i%2 == 0
        downsampled.add(upsampled.points[i/2][0], upsampled.points[i/2][1])
      else
        downsampled.add((upsampled.points[i/2][0] + upsampled.points[i/2 + 1][0])*1.0/2, (upsampled.points[i/2][1] + upsampled.points[i/2 + 1][1])*1.0/2)
      end
    }
    return downsampled
  end

  def wavelet_decomposition # performs wavelet decomposition
    @points.push(@points[-1]) if @points.size%2 == 1
    general = Points.new(@name + "_general")
    wavelet = Points.new(@name + "_wavelet")
    (@points.size/2).times{ |i|
      general.add(@points[i*2][0], (@points[i*2][1] + @points[i*2 + 1][1])*1.0/2)
      wavelet.add(@points[i*2 + 1][0], (@points[i*2][1] - @points[i*2 + 1][1])*1.0/2)
    }
    # to add the last point?
    return general, wavelet
  end

  def plus_wavelet(wavelet) # adds values, supposed that wavelet received by 'wavelet_decomposition'
    init_points = Points.new(@name + "+wavelet")
    @points.size.times{ |i|
      init_points.add(@points[i][0], @points[i][1] + wavelet.points[i][1])
      init_points.add(wavelet.points[i][0], @points[i][1] - wavelet.points[i][1])
    }
    return init_points
  end

  def deviate(value) # returns mutated points
    deviated = Points.new(@name + "_deviated")
    @points.each{ |point|
      deviated.add(point[0], point[1] + rand(value) - value/2)
    }
    return deviated
  end

  def adjust_points
    # remove redundant points
    if @points.size > 2
      to_delete = []
      (@points.size - 2).times{ |i|
        x_0 = @points[i]
        x_1 = @points[i + 1]
        x_2 = @points[i + 2]
        if (x_1[1] - x_0[1])*1.0/(x_1[0] - x_0[0]) == (x_2[1] - x_1[1])*1.0/(x_2[0] - x_1[0])
          to_delete.push([x_1[0], x_1[1]])
        end
      }
      to_delete.each{ |elem|
        self.delete(elem[0], elem[1])
      }
    end
  end

  def draw(file) # creates a file with graphs
    DrawCharts.new(file, self)
  end
end

#TODO: improve Function
class Function
  def initialize(func)
    @func = func
  end

  def handle_value(value)
    x = value
    y = case @func
      when "sin"   then Math.sin(value)
      when "cos"   then Math.cos(value)
      when "tan"   then Math.tan(value)
      when "asin"  then Math.asin(value)
      when "acos"  then Math.acos(value)
      when "atan"  then Math.atan(value)
      when "exp"   then Math.exp(value)
      when "sqrt"  then Math.sqrt(value)
      when "hypot" then Math.hypot(value)
      else value
    end
    return x, y
  end

  def create_points(interval, step, name="function")
    if interval.size != 2
      puts "Incorrect interval"
    elsif step.to_i != step && step.to_f != step
      puts "Incorrect step"
    else
      points = Points.new(name)
      (((interval[1] - interval[0])/step).to_i + 1).times{ |i|
        x, y = handle_value(interval[0] + i*step)
        points.add(x, y)
      }
      # add last point if step chosen sloppy
      if ((interval[1] - interval[0])/step).to_i != (interval[1] - interval[0])/step
        x, y = handle_value(interval[1])
        points.add(x, y)
      end
      return points
    end
  end
end

class DrawCharts # draws graphs
  def initialize(file, *args)
    require 'RMagick'

    imgl = Magick::ImageList.new

    _max_x = nil
    _min_x = nil
    _max_y = nil
    _min_y = nil
    for elem in args
      _max_x = elem.max_x if _max_x.nil? || elem.max_x > _max_x
      _min_x = elem.min_x if _min_x.nil? || elem.min_x < _min_x
      _max_y = elem.max_y if _max_y.nil? || elem.max_y > _max_y
      _min_y = elem.min_y if _min_y.nil? || elem.min_y < _min_y
    end

    x = [_max_x - _min_x, _min_x.abs, _max_x].max
    y = [_max_y - _min_y, _min_y.abs, _max_y].max
    round_coeff = 2
    _max_x = _max_x.round_to(round_coeff) if _max_x != _max_x.round
    _min_x = _min_x.round_to(round_coeff) if _min_x != _min_x.round
    _max_y = _max_y.round_to(round_coeff) if _max_y != _max_y.round
    _min_y = _min_y.round_to(round_coeff) if _min_y != _min_y.round

    border = 30
    closure = border - 5
    legend_str = 13
    max = [_min_x.abs, _max_x, _min_y.abs, _max_y].max
    display_param = 500 # should be chosen experimentally
    coeff = max < display_param ? display_param/max : 1 # coeff to scale
    x = x*coeff + 2*border
    y = y*coeff + 2*border + legend_str*(args.size - 1)

    imgl.new_image(x, y, Magick::HatchFill.new('white','lightcyan2'))
    gc = Magick::Draw.new

    #axises painting
    gc.affine(1, 0, 0, -1, x - [_max_x*coeff, 0].max - border, [_max_y*coeff, 0].max + border)
    gc.stroke('gray')
    gc.fill('gray')
    gc.stroke_width(1)
    #axis y
    gc.polyline(0,[_min_y*coeff, 0].min - closure,
      0,[_max_y*coeff,0].max + closure,
      -2,[_max_y*coeff,0].max + closure - 10,
      2,[_max_y*coeff,0].max + closure - 10,
      0,[_max_y*coeff,0].max + closure)
    #axis x
    gc.polyline([_min_x*coeff, 0].min - closure,0,
      [_max_x*coeff,0].max + closure,0,
      [_max_x*coeff,0].max + closure - 10,-2,
      [_max_x*coeff,0].max + closure - 10,2,
      [_max_x*coeff,0].max + closure,0)
    gc.draw(imgl)

    text_gc = Magick::Draw.new
    text_gc.pointsize(10)
    text_gc.font_weight(Magick::NormalWeight)
    text_gc.stroke('transparent')
    text_gc.affine(1, 0, 0, 1, x - [_max_x*coeff, 0].max - border, [_max_y*coeff, 0].max + border)
    #zero point
    zero = "0"
    if _min_x > 0
      text_gc.text(-16, 10, zero)
    elsif _max_y < 0
      text_gc.text(-16, -2, zero)
    else
      text_gc.text(3, 10, zero)
    end
    #max x
    text_gc.text(_max_x*coeff - _max_x.to_s.size*3, 11, _max_x.to_s) if _max_x != 0
    text_gc.line(_max_x*coeff, 1, _max_x*coeff, -1)
    #min x
    text_gc.text(_min_x*coeff - _min_x.to_s.size*3, 11, _min_x.to_s) if _min_x != 0
    text_gc.line(_min_x*coeff, 1, _min_x*coeff, -1)
    #max y
    text_gc.text(3, -_max_y*coeff + 4, _max_y.to_s) if _max_y != 0
    text_gc.line(1, -_max_y*coeff, -1, -_max_y*coeff)
    #min y
    text_gc.text(3, -_min_y*coeff + 4, _min_y.to_s) if _min_y != 0
    text_gc.line(1, -_min_y*coeff, -1, -_min_y*coeff)
    #x and y
    text_gc.text([_max_x*coeff, 0].max - 16 + closure, -1, "x")
    text_gc.text(3, [-_max_y*coeff, 0].min + 18 - closure, "y")
    text_gc.draw(imgl)

    graph = Magick::Draw.new
    graph.pointsize(12)
    #graph.font_weight(100)
    graph.stroke_width(1)
    graph.affine(1, 0, 0, -1, x - [_max_x*coeff, 0].max - border, [_max_y*coeff, 0].max + border)
    color = ["red", "blue", "green", "orange", "pink", "yellow", "brown"]
    ind = 0
    args.each{ |elem|
      # graph drawing
      _points = elem.points
      graph.stroke(color[ind%color.size])
      (_points.size - 1).times{|j|
        graph.line(_points[j][0]*coeff, _points[j][1]*coeff, _points[j + 1][0]*coeff, _points[j + 1][1]*coeff)
      }

      # line example in the legend
      graph.affine(1, 0, 0, -1, 0, 0)
      graph.line([_min_x*coeff, 0].min - closure, [-_min_y*coeff, 0].max + closure + legend_str*ind - 4,
        [_min_x*coeff, 0].min, [-_min_y*coeff, 0].max + closure + legend_str*ind - 4)

      # graph name
      graph.stroke("black")
      graph.text([_min_x*coeff, 0].min + closure/4, [-_min_y*coeff, 0].max + closure + legend_str*ind, elem.name)
      
      graph.draw(imgl)
      ind += 1
      graph.affine(1, 0, 0, -1, 0, 0)
    }
    imgl.write(file)
  end
end

puts "Start" + Time.new.to_s

begin # simple examples
  p = Points.new("graph_1")
  p.add(-100, -100)
  p.add(-70, -70)
  p.add(-60, -60)
  p.add(-50, -50)
  p.add(-40, -40)
  p.add(-5, -5)
  p.add(2, 30)
  p.add(15, 15)
  p.add(6, 6)
  p.add(7, 7)
  p.add(8, 8)
  p.add(9, 9)
  p.add(10, 10)
  p p.points
  p.adjust_points
  p p.points
  p.draw("dev.png")
end

puts "Finish" + Time.new.to_s